using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using WpfApp3.Tools;
using WpfApp3.Tools.Helper;

namespace WpfApp3.Controls;

public class TabItem : System.Windows.Controls.TabItem
{
    private const int AnimationSpeed = 150;
    private const double WaitLength = 20;

    private bool _isWaiting;
    private bool _isDragging;
    private bool _isDragged;
    private Point _dragPoint;
    private int _mouseDownIndex;
    private double _mouseDownOffsetX;
    private double _mouseDownOffsetY;
    private Point _mouseDownPoint;
    private double _maxMoveRight;
    private double _maxMoveButtom;
    private double _maxMoveLeft;
    private double _maxMoveTop;
    private int _currentIndex;
    private double _scrollHorizontalOffset;
    private double _scrollVerticalOffset;
    private TabPanel _tabPanel;

    public double ItemWidth { get; internal set; }
    public double ItemHeight { get; internal set; }
    internal double TargetOffsetX { get; set; }
    internal double TargetOffsetY { get; set; }

    internal TabPanel TabPanel
    {
        get
        {
            if (_tabPanel == null && TabControlParent != null)
            {
                _tabPanel = TabControlParent.HeaderPanel;
            }

            return _tabPanel;
        }
        set => _tabPanel = value;
    }

    internal int CurrentIndex
    {
        get => _currentIndex;
        set
        {
            if (_currentIndex == value || value < 0) return;
            var oldIndex = _currentIndex;
            _currentIndex = value;
            UpdateItemOffset(oldIndex);
        }
    }

    public static readonly DependencyProperty IconProperty = DependencyProperty.Register(
        nameof(Icon), 
        typeof(object), 
        typeof(TabItem), 
        new PropertyMetadata(null));

    public object? Icon
    {
        get => GetValue(IconProperty);
        set => SetValue(IconProperty, value);
    }

    public static readonly DependencyProperty ShowCloseButtonProperty = 
        TabControl.ShowCloseButtonProperty.AddOwner(typeof(TabItem));

    public bool ShowCloseButton
    {
        get => (bool)GetValue(ShowCloseButtonProperty);
        set => SetValue(ShowCloseButtonProperty, value);
    }

    public static readonly DependencyProperty ShowContextMenuProperty = 
        TabControl.ShowContextMenuProperty.AddOwner(
            typeof(TabItem), 
            new FrameworkPropertyMetadata(OnShowContextMenuChanged));

    public bool ShowContextMenu
    {
        get => (bool)GetValue(ShowContextMenuProperty);
        set => SetValue(ShowContextMenuProperty, value);
    }

    public static readonly DependencyProperty MenuProperty = DependencyProperty.Register(
        nameof(Menu), 
        typeof(ContextMenu), 
        typeof(TabItem), 
        new PropertyMetadata(default(ContextMenu), OnMenuChanged));

    public ContextMenu Menu
    {
        get => (ContextMenu)GetValue(MenuProperty);
        set => SetValue(MenuProperty, value);
    }

    private TabControl TabControlParent => 
        (TabControl)ItemsControl.ItemsControlFromItemContainer(this);

    public TabItem()
    {
        CommandBindings.Add(new CommandBinding(ControlCommands.Close, (s, e) => Close()));
        CommandBindings.Add(new CommandBinding(ControlCommands.CloseAll, (s, e) => TabControlParent.CloseAllItems()));
        CommandBindings.Add(new CommandBinding(
            ControlCommands.CloseOther,
            (s, e) => TabControlParent.CloseOtherItems(this),
            (s, e) => e.CanExecute = TabControlParent.Items.Count > 1));

        Loaded += (s, e) => OnMenuChanged(Menu);
    }

    private static void OnShowContextMenuChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is TabItem item && item.Menu != null)
        {
            var show = (bool)e.NewValue;
            item.Menu.IsEnabled = show;
            item.Menu.Show(show);
        }
    }

    private static void OnMenuChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is TabItem item)
        {
            item.OnMenuChanged(e.NewValue as ContextMenu);
        }
    }

    private void OnMenuChanged(ContextMenu menu)
    {
        if (!IsLoaded || menu == null) return;

        var parent = TabControlParent;
        if (parent == null) return;

        var item = parent.ItemContainerGenerator.ItemFromContainer(this);
        menu.DataContext = item;
        
        menu.SetBinding(IsEnabledProperty, new Binding(ShowContextMenuProperty.Name) { Source = this });
        menu.SetBinding(VisibilityProperty, new Binding(ShowContextMenuProperty.Name)
        {
            Source = this,
            Converter = ResourceHelper.GetResourceInternal<IValueConverter>("Boolean2VisibilityConverter")
        });
    }

    private void UpdateItemOffset(int oldIndex)
    {
        if (!_isDragging || CurrentIndex >= TabPanel.ItemDic.Count) return;

        var moveItem = TabPanel.ItemDic[CurrentIndex];
        moveItem.CurrentIndex -= CurrentIndex - oldIndex;

        var isVertical = TabControlParent.TabPosition == TabPosition.Left || TabControlParent.TabPosition == TabPosition.Right;
        if (isVertical)
        {
            var offset = moveItem.TargetOffsetY;
            var result = offset + (oldIndex - CurrentIndex) * ItemHeight;
            TabPanel.ItemDic[CurrentIndex] = this;
            TabPanel.ItemDic[moveItem.CurrentIndex] = moveItem;
            moveItem.CreateAnimation(offset, result);
        }
        else
        {
            var offset = moveItem.TargetOffsetX;
            var result = offset + (oldIndex - CurrentIndex) * ItemWidth;
            TabPanel.ItemDic[CurrentIndex] = this;
            TabPanel.ItemDic[moveItem.CurrentIndex] = moveItem;
            moveItem.CreateAnimation(offset, result);
        }
    }

    internal void Close()
    {
        var parent = TabControlParent;
        if (parent == null) return;

        var item = (TabItem)parent.ItemContainerGenerator.ItemFromContainer(this);
        var argsClosing = new ClosingEventArgs(item);
        RaiseEvent(argsClosing);
        if (argsClosing.Cancel) return;

        TabPanel.SetValue(TabPanel.FluidMoveDurationPropertyKey, 
            parent.IsAnimationEnabled ? new Duration(TimeSpan.FromMilliseconds(200)) : new Duration(TimeSpan.FromMilliseconds(1)));

        parent.IsInternalAction = true;
        RaiseEvent(new ClosedEventArgs(item));

        var list = parent.GetActualList();
        list?.Remove(item);
    }

    protected override void OnMouseRightButtonDown(MouseButtonEventArgs e)
    {
        base.OnMouseRightButtonDown(e);
        if (!IsMouseOverHeader(e)) return;
        IsSelected = true;
        Focus();
    }

    protected override void OnHeaderChanged(object oldHeader, object newHeader)
    {
        base.OnHeaderChanged(oldHeader, newHeader);
        if (TabPanel != null)
        {
            TabPanel.ForceUpdate = true;
            InvalidateMeasure();
            TabPanel.ForceUpdate = true;
        }
    }

    protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
    {
        base.OnMouseLeftButtonDown(e);
        if (!IsMouseOverHeader(e)) return;

        var parent = TabControlParent;
        if (parent?.IsDraggable != true || _isDragging) return;

        InitializeDragOperation(e);
    }

    private void InitializeDragOperation(MouseButtonEventArgs e)
    {
        var parent = TabControlParent;
        parent.UpdateScroll();
        TabPanel.SetValue(TabPanel.FluidMoveDurationPropertyKey, new Duration(TimeSpan.FromSeconds(0)));

        var isVertical = parent.TabPosition == TabPosition.Left || parent.TabPosition == TabPosition.Right;
        if (isVertical)
        {
            InitializeVerticalDrag(e);
        }
        else
        {
            InitializeHorizontalDrag(e);
        }

        _mouseDownPoint = _dragPoint;
        CaptureMouse();
    }

    private void InitializeVerticalDrag(MouseButtonEventArgs e)
    {
        var parent = TabControlParent;
        _mouseDownOffsetY = RenderTransform.Value.OffsetY;
        _scrollVerticalOffset = parent.GetVerticalOffset();
        var mx = TranslatePoint(new Point(), parent).Y + _scrollVerticalOffset;
        _mouseDownIndex = CalLocationIndex(mx);
        var subIndex = _mouseDownIndex - CalLocationIndex(_scrollVerticalOffset);
        _maxMoveTop = -subIndex * ItemHeight;
        _maxMoveButtom = parent.ActualHeight - ActualHeight + _maxMoveTop;
        _isDragging = true;
        _isWaiting = true;
        _dragPoint = e.GetPosition(parent);
        _dragPoint = new Point(_dragPoint.X, _dragPoint.Y + _scrollVerticalOffset);
    }

    private void InitializeHorizontalDrag(MouseButtonEventArgs e)
    {
        var parent = TabControlParent;
        _mouseDownOffsetX = RenderTransform.Value.OffsetX;
        _scrollHorizontalOffset = parent.GetHorizontalOffset();
        var mx = TranslatePoint(new Point(), parent).X + _scrollHorizontalOffset;
        _mouseDownIndex = CalLocationIndex(mx);
        var subIndex = _mouseDownIndex - CalLocationIndex(_scrollHorizontalOffset);
        _maxMoveLeft = -subIndex * ItemWidth;
        _maxMoveRight = parent.ActualWidth - ActualWidth + _maxMoveLeft;
        _isDragging = true;
        _isWaiting = true;
        _dragPoint = e.GetPosition(parent);
        _dragPoint = new Point(_dragPoint.X + _scrollHorizontalOffset, _dragPoint.Y);
    }

    protected override void OnMouseMove(MouseEventArgs e)
    {
        base.OnMouseMove(e);
        if (!_isDragging) return;

        var parent = TabControlParent;
        if (parent == null) return;

        var isVertical = parent.TabPosition == TabPosition.Left || parent.TabPosition == TabPosition.Right;
        if (isVertical)
        {
            HandleVerticalDrag(e);
        }
        else
        {
            HandleHorizontalDrag(e);
        }
    }

    private void HandleVerticalDrag(MouseEventArgs e)
    {
        var parent = TabControlParent;
        var offset = TranslatePoint(new Point(), parent).Y + _scrollVerticalOffset;
        CurrentIndex = CalLocationIndex(offset);

        var p = e.GetPosition(parent);
        p = new Point(p.X, p.Y + _scrollVerticalOffset);

        var subTop = p.Y - _dragPoint.Y;
        var totalTop = p.Y - _mouseDownPoint.Y;

        if (Math.Abs(subTop) <= WaitLength && _isWaiting) return;

        _isWaiting = false;
        _isDragged = true;

        var top = subTop + RenderTransform.Value.OffsetY;
        if (totalTop < _maxMoveTop)
        {
            top = _maxMoveTop + _mouseDownOffsetY;
        }
        else if (totalTop > _maxMoveButtom)
        {
            top = _maxMoveButtom + _mouseDownOffsetY;
        }

        RenderTransform = new TranslateTransform(0, top);
        _dragPoint = p;
    }

    private void HandleHorizontalDrag(MouseEventArgs e)
    {
        var parent = TabControlParent;
        var offset = TranslatePoint(new Point(), parent).X + _scrollHorizontalOffset;
        CurrentIndex = CalLocationIndex(offset);

        var p = e.GetPosition(parent);
        p = new Point(p.X + _scrollHorizontalOffset, p.Y);

        var subLeft = p.X - _dragPoint.X;
        var totalLeft = p.X - _mouseDownPoint.X;

        if (Math.Abs(subLeft) <= WaitLength && _isWaiting) return;

        _isWaiting = false;
        _isDragged = true;

        var left = subLeft + RenderTransform.Value.OffsetX;
        if (totalLeft < _maxMoveLeft)
        {
            left = _maxMoveLeft + _mouseDownOffsetX;
        }
        else if (totalLeft > _maxMoveRight)
        {
            left = _maxMoveRight + _mouseDownOffsetX;
        }

        RenderTransform = new TranslateTransform(left, 0);
        _dragPoint = p;
    }

    protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
    {
        base.OnMouseLeftButtonUp(e);
        ReleaseMouseCapture();

        if (_isDragged)
        {
            FinalizeDragOperation();
        }

        _isDragging = false;
        _isDragged = false;
    }

    private void FinalizeDragOperation()
    {
        var parent = TabControlParent;
        if (parent == null) return;

        var isVertical = parent.TabPosition == TabPosition.Left || parent.TabPosition == TabPosition.Right;
        if (isVertical)
        {
            var subY = TranslatePoint(new Point(), parent).Y + _scrollVerticalOffset;
            var index = CalLocationIndex(subY);
            var top = index * ItemHeight;
            var offset = RenderTransform.Value.OffsetY;
            CreateAnimation(offset, offset - subY + top, index);
        }
        else
        {
            var subX = TranslatePoint(new Point(), parent).X + _scrollHorizontalOffset;
            var index = CalLocationIndex(subX);
            var left = index * ItemWidth;
            var offset = RenderTransform.Value.OffsetX;
            CreateAnimation(offset, offset - subX + left, index);
        }
    }

    protected override void OnMouseDown(MouseButtonEventArgs e)
    {
        if (e is { ChangedButton: MouseButton.Middle, ButtonState: MouseButtonState.Pressed } &&
            TabControlParent.CanBeClosedByMiddleButton &&
            IsMouseOverHeader(e) &&
            (ShowCloseButton || ShowContextMenu))
        {
            Close();
        }
    }

    internal void CreateAnimation(double offset, double result, int index = -1)
    {
        var parent = TabControlParent;
        var isVertical = parent.TabPosition == TabPosition.Left || parent.TabPosition == TabPosition.Right;

        if (isVertical)
        {
            TargetOffsetY = result;
        }
        else
        {
            TargetOffsetX = result;
        }

        if (!parent.IsAnimationEnabled)
        {
            AnimationCompleted(result, index, isVertical);
            return;
        }

        var animation = AnimationHelper.CreateAnimation(result, AnimationSpeed);
        animation.FillBehavior = FillBehavior.Stop;
        animation.Completed += (s1, e1) => AnimationCompleted(result, index, isVertical);

        var transform = new TranslateTransform(
            isVertical ? 0 : offset,
            isVertical ? offset : 0);
        RenderTransform = transform;

        if (isVertical)
        {
            transform.BeginAnimation(TranslateTransform.YProperty, animation, HandoffBehavior.Compose);
        }
        else
        {
            transform.BeginAnimation(TranslateTransform.XProperty, animation, HandoffBehavior.Compose);
        }
    }

    private void AnimationCompleted(double result, int index, bool isVertical)
    {
        RenderTransform = new TranslateTransform(
            isVertical ? 0 : result,
            isVertical ? result : 0);

        if (index == -1) return;

        var parent = TabControlParent;
        var list = parent.GetActualList();
        if (list == null) return;

        var item = parent.ItemContainerGenerator.ItemFromContainer(this);
        if (item == null) return;

        TabPanel.CanUpdate = false;
        parent.IsInternalAction = true;

        list.Remove(item);
        parent.IsInternalAction = true;
        list.Insert(index, item);
        _tabPanel.SetValue(TabPanel.FluidMoveDurationPropertyKey, new Duration(TimeSpan.FromMilliseconds(0)));
        TabPanel.CanUpdate = true;
        TabPanel.ForceUpdate = true;

        TabPanel.Measure(new Size(
            isVertical ? ActualWidth : TabPanel.DesiredSize.Width,
            isVertical ? TabPanel.DesiredSize.Height : ActualHeight));

        TabPanel.ForceUpdate = false;

        Focus();
        IsSelected = true;
        if (!IsMouseCaptured)
        {
            parent.SetCurrentValue(Selector.SelectedIndexProperty, _currentIndex);
        }
    }

    private int CalLocationIndex(double offset)
    {
        if (_isWaiting) return CurrentIndex;

        var isVertical = TabControlParent.TabPosition == TabPosition.Left || TabControlParent.TabPosition == TabPosition.Right;
        var maxIndex = TabControlParent.Items.Count - 1;
        var div = (int)(offset / (isVertical ? ItemHeight : ItemWidth));

        return Math.Min(div, maxIndex);
    }

    private bool IsMouseOverHeader(MouseButtonEventArgs e) => 
        VisualTreeHelper.HitTest(this, e.GetPosition(this)) is not null;
}
